import React, { useMemo } from 'react';
import pick from 'lodash/pick';
import isNil from 'lodash/isNil';
import isFunction from 'lodash/isFunction';
import useTreeWithChildren from '../Tree/hooks/useTreeWithChildren';
import useFlattenTree from '../Tree/hooks/useFlattenTree';
import useFocusState from './hooks/useFocusState';
import useExpandTree from '../Tree/hooks/useExpandTree';
import TreeView, { TreeViewProps } from '../Tree/TreeView';
import { PickerLocale } from '../locales';
import { useStyles, useCustom, useControlled, useEventCallback } from '@/internals/hooks';
import { forwardRef, createChainedFunction, mergeRefs } from '@/internals/utils';
import { getActiveItem, getTreeActiveNode } from '../Tree/utils';
import {
  PickerToggle,
  PickerPopup,
  PickerToggleTrigger,
  usePickerRef,
  onMenuKeyDown,
  triggerPropKeys,
  PositionChildProps,
  useToggleKeyDownEvent,
  PickerToggleProps
} from '@/internals/Picker';
import { isLeafNode } from '@/internals/Tree/utils';
import { TreeProvider, useTreeImperativeHandle } from '@/internals/Tree/TreeProvider';
import { TreeNode } from '@/internals/Tree/types';
import type { FormControlPickerProps, DeprecatedMenuProps } from '@/internals/types';
import type { TreeExtraProps } from '../Tree/types';

export interface TreePickerProps<V = number | string | null>
  extends TreeViewProps<V>,
    TreeExtraProps,
    DeprecatedMenuProps,
    FormControlPickerProps<V, PickerLocale, TreeNode>,
    Pick<PickerToggleProps, 'caretAs' | 'loading'> {
  /**
   * Custom render selected items
   */
  renderValue?: (
    value: V,
    selectedNode: TreeNode,
    selectedElement: React.ReactNode
  ) => React.ReactNode;

  /**
   * The height of the tree
   */
  treeHeight?: number;

  /**
   * Popup auto width
   *
   * @default true
   */
  popupAutoWidth?: boolean;

  /**
   * Whether only leaf nodes can be selected
   *
   * @default false
   */
  onlyLeafSelectable?: boolean;
}

/**
 * The `TreePicker` component is used for selecting single options which are organized in a tree structure.
 *
 * @see https://rsuitejs.com/components/tree-picker/
 */
const TreePicker = forwardRef<'div', TreePickerProps>((props, ref) => {
  const { propsWithDefaults } = useCustom('TreePicker', props);
  const {
    as,
    appearance = 'default',
    classPrefix = 'picker',
    cleanable = true,
    childrenKey = 'children',
    data = [],
    disabled,
    defaultValue,
    defaultExpandAll = false,
    disabledItemValues = [],
    defaultExpandItemValues = [],
    expandItemValues: controlledExpandItemValues,
    id,
    block,
    className,
    locale,
    labelKey = 'label',
    onlyLeafSelectable,
    placeholder,
    placement = 'bottomStart',
    style,
    searchKeyword,
    searchable = true,
    showIndentLine,
    popupClassName,
    popupStyle,
    popupAutoWidth = true,
    treeHeight = 320,
    valueKey = 'value',
    virtualized = false,
    value: controlledValue,
    listProps,
    toggleAs,
    searchBy,
    getChildren,
    onClean,
    onSearch,
    onSelect,
    onSelectItem,
    onChange,
    onExpand,
    onEnter,
    onExit,
    onEntered,
    renderValue,
    renderTree,
    renderTreeIcon,
    renderTreeNode,
    renderExtraFooter,
    ...rest
  } = propsWithDefaults;

  const { trigger, root, target, overlay, list, searchInput, treeView } = usePickerRef(ref);
  const [value, setValue] = useControlled(controlledValue, defaultValue);
  const itemDataKeys = { childrenKey, labelKey, valueKey };

  const { treeData, loadingNodeValues, appendChild } = useTreeWithChildren(data, itemDataKeys);
  const flattenedNodes = useFlattenTree(treeData, { ...itemDataKeys });

  const { expandItemValues, handleExpandTreeNode } = useExpandTree(data, {
    ...itemDataKeys,
    defaultExpandAll,
    defaultExpandItemValues,
    controlledExpandItemValues,
    onExpand,
    getChildren,
    appendChild
  });

  const { prefix, merge } = useStyles(classPrefix);
  const activeNode = getTreeActiveNode(flattenedNodes, value, valueKey);

  const { register, focusFirstNode, focusActiveNode } = useTreeImperativeHandle();
  const { active, focusItemValue, setFocusItemValue, triggerProps } = useFocusState({
    focusActiveNode,
    target,
    value,
    onEnter,
    onExit,
    onEntered
  });

  const handleSelect = useEventCallback(
    (treeNode: TreeNode, value: string | number | null, event: React.SyntheticEvent) => {
      onSelect?.(treeNode, value, event);

      // Only leaf nodes can update the value and close the picker.
      if (onlyLeafSelectable && !isLeafNode(treeNode)) {
        return;
      }

      setFocusItemValue(value);
      handleChange(value, event);

      target.current?.focus();
      trigger.current?.close?.();
    }
  );

  const handleClean = useEventCallback((event: React.SyntheticEvent) => {
    const target = event.target as Element;
    // exclude searchbox
    if (target.matches('input[role="searchbox"]') || disabled || !cleanable) {
      return;
    }
    setValue(null);
    onChange?.(null, event);
  });

  const handleTreePressEnter = useEventCallback((event: React.SyntheticEvent) => {
    if (isNil(focusItemValue)) {
      return;
    }

    const activeItem = getActiveItem(focusItemValue, flattenedNodes, valueKey);

    handleSelect(activeItem, focusItemValue, event);
  });

  const handleTreeKeyDown = useEventCallback((event: React.KeyboardEvent<any>) => {
    onMenuKeyDown(event, {
      del: handleClean,
      down: () => focusFirstNode(),
      enter: handleTreePressEnter
    });
  });

  const onPickerKeydown = useToggleKeyDownEvent({
    toggle: !activeNode || !active,
    trigger,
    target,
    overlay,
    searchInput,
    active,
    onExit: handleClean,
    onMenuKeyDown: handleTreeKeyDown,
    ...rest
  });

  const handleChange = useEventCallback(
    (nextValue: string | number | null, event: React.SyntheticEvent) => {
      setValue(nextValue);
      onChange?.(nextValue, event);
    }
  );

  const treeContext = useMemo(
    () => ({
      register,
      props: { labelKey, valueKey, childrenKey, virtualized, renderTreeIcon, renderTreeNode }
    }),
    [childrenKey, labelKey, valueKey, virtualized, register, renderTreeIcon, renderTreeNode]
  );

  const tree = (
    <TreeProvider value={treeContext}>
      <TreeView
        ref={treeView}
        value={value}
        data={treeData}
        disabledItemValues={disabledItemValues}
        expandItemValues={expandItemValues}
        showIndentLine={showIndentLine}
        searchable={searchable}
        searchKeyword={searchKeyword}
        searchBy={searchBy}
        searchInputRef={searchInput}
        loadingNodeValues={loadingNodeValues}
        flattenedNodes={flattenedNodes}
        listProps={listProps}
        listRef={list}
        locale={locale}
        height={treeHeight}
        onExpand={handleExpandTreeNode}
        onSearch={onSearch}
        onSelect={handleSelect}
        onSelectItem={onSelectItem}
        onFocusItem={setFocusItemValue}
      />
    </TreeProvider>
  );

  const renderTreeView = (positionProps: PositionChildProps, speakerRef) => {
    const { className } = positionProps;
    const classes = merge(className, popupClassName, prefix('tree-menu'));

    return (
      <PickerPopup
        autoWidth={popupAutoWidth}
        className={classes}
        style={popupStyle}
        ref={mergeRefs(overlay, speakerRef)}
        onKeyDown={onPickerKeydown}
        target={trigger}
      >
        {renderTree ? renderTree(tree) : tree}
        {renderExtraFooter?.()}
      </PickerPopup>
    );
  };

  /**
   * 1.Have a value and the value is valid.
   * 2.Regardless of whether the value is valid, as long as renderValue is set, it is judged to have a value.
   */
  let hasValidValue = !isNil(activeNode) || (!isNil(value) && isFunction(renderValue));
  let selectedElement: React.ReactNode = placeholder;

  if (hasValidValue) {
    const node = activeNode ?? {};
    selectedElement = node[labelKey];
    if (isFunction(renderValue) && value) {
      selectedElement = renderValue(value, node, selectedElement);
      if (isNil(selectedElement)) {
        hasValidValue = false;
      }
    }
  }

  return (
    <PickerToggleTrigger
      as={as}
      id={id}
      pickerType="tree"
      block={block}
      disabled={disabled}
      appearance={appearance}
      popupType="tree"
      triggerProps={{
        ...pick(props, triggerPropKeys),
        ...triggerProps
      }}
      ref={trigger}
      placement={placement}
      speaker={renderTreeView}
      rootRef={root}
      style={style}
      classPrefix={classPrefix}
      className={className}
    >
      <PickerToggle
        ref={target}
        appearance={appearance}
        onKeyDown={onPickerKeydown}
        onClean={createChainedFunction(handleClean, onClean)}
        cleanable={cleanable && !disabled}
        as={toggleAs}
        disabled={disabled}
        hasValue={hasValidValue}
        active={active}
        placement={placement}
        inputValue={value}
        focusItemValue={focusItemValue}
        {...rest}
      >
        {selectedElement || locale?.placeholder}
      </PickerToggle>
    </PickerToggleTrigger>
  );
});

TreePicker.displayName = 'TreePicker';

export default TreePicker;
